最後終於來到了我們最後一個章節:『 Transforms 』。
Transform 在 slate package 裡頭也是佔了舉足輕重的地位,它提供了最 high-level 操作 Slate editor 的方法,讓開發者在了解 Slate 的基本運作概念以後就能直接透過這些 apis 無痛開發。
但 High-level 的 Transform 與 Low-level 的 Operation 之間還是存在著差距的,即便名義上是說一組 Transform 是由複數個 Operations 所組成,我們仍不難猜想在各個 Transform methods 裡一定有事先為我們擋掉了各種 edge-cases 甚至下了不少優化操作效能的功夫。
如果我們選擇每個 Transform methods 都深入探討裡頭的程式碼的話 ... 另一個 30 天可能又過去了吧 ...
經過筆者縝密的思考(其實就是想省點事而已XD)後,決定讓這一整個章節以偏向 reference 的形式進行,剛好官方 document 上對各個 methods 的介紹也不太完整,因此這邊會盡可能補足官方 document 遺漏解釋的內容,會以介紹各個 Transform methods 的功用以及傳入的參數的用途為主。所以除非解釋上必較,否則我們會盡可能不過度深入程式碼的內容。
以下我們先從整個 NodeTrasforms
的通用 Options
開始介紹起。
interface NodeOptions {
at?: Location
match?: NodeMatch<T>
mode?: ('highest' | 'lowest') | ('all' | 'highest' | 'lowest')
voids?: boolean
}
at
: 欲將節點插入編輯器的 Location ,預設值為編輯器的 selection
value
match
: 自定義的 match
function ,詳細的解釋請看 Day 12
mode
:這個參數主要用於 Editor.nodes
method 的 mode
option ,以 insertNodes
為例:
insertNodes(...) {
// ...
const [entry] = Editor.nodes(editor, {
at: at.path,
match,
mode,
voids,
})
// ...
}
決定 Editor.nodes
method 要以哪種模式遍歷 Slate node tree 。它分成三種模式:
'all'
從 at
出發,以正常的『垂直遍歷』的方式 yield
出查找到的節點
'highest'
只會查找並 yield
出最淺層的節點
'lowest'
只會查找並 yield
出 at
所涵蓋的 branch 最底層的節點
voids
決定在這個 Transform method 有呼叫到的所有操作行為中,是否要略過或避開 void nodes 的出現 。
insertNodes
用途:『插入單一/複數節點進編輯器』
參數:
editor: Editor
nodes: Node | Node[]
options: InsertNodesOptions
options :
interface InsertNodesOptions extends NodeOptions {
hanging?: boolean
select?: boolean
}
hanging
如果傳入的 at
為 Range type 的話,這個 value 會決定 Range 是否要另外修正為 unhanging
type 。
hanging 在 Slate 裡頭的意思代表『這段 Range 涵蓋到了不存在的節點』。
我們假設目前的 Slate Document 如下:
[{text: 'one '}, {text: 'two', bold: true}, {text: ' three'}]
這時使用者看到的顯示方式應該如下:
one two three
假設使用者選取了 "two" ,此時的 selection
會有幾種 anchor
與 focus
points 的可能性出現
// 1 -- no hanging
{
anchor: { path: [1], offset: 0 },
focus: { path: [1], offset: 3 }
}
// 2 -- anchor hanging
{
anchor: { path: [0], offset: 4 },
focus: { path: [1], offset: 3 }
}
// 3 -- focus hanging
{
anchor: { path: [1], offset: 0 },
focus: { path: [2], offset: 0 }
}
// 4 -- both hanging
{
anchor: { path: [0], offset: 4 },
focus: { path: [2], offset: 0 }
}
當我們傳入 hanging: false
時, Slate 就會將這組 Range 傳入 Editor.unhangRange
method 裡確保 Range 維持在第一種情形。
select
決定是否更新編輯器的 selection
,如果沒有傳入 at
參數,以編輯器的 selection
位置去新增節點的話則會強制將 select
設為 true
。
liftNodes
用途:將特定 Location 指向的內容,於 Document tree 裡向上提升一個層級。如果有必要的話會將它的父層節點一分為二。
參數
editor: Editor
options: LiftNodesOptions
options :
interface LiftNodesOptions extends NodeOptions {}
這個 method 限制無法提升路徑長度小於 2 的節點(路徑長度為 1 的節點上層就是 Editor root 了)
if (path.length < 2) {
throw new Error(
`Cannot lift node at a path [${path}] because it has a depth of less than \`2\`.`
)
}
這個 method 又可以分成四種可能性:
if (length === 1) {
const toPath = Path.next(parentPath)
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
Transforms.removeNodes(editor, { at: parentPath, voids })
} else if (index === 0) {
Transforms.moveNodes(editor, { at: path, to: parentPath, voids })
} else if (index === length - 1) {
const toPath = Path.next(parentPath)
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
} else {
const splitPath = Path.next(path)
const toPath = Path.next(parentPath)
Transforms.splitNodes(editor, { at: splitPath, voids })
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
}
mergeNodes
用途:將特定 Location 指向的內容,與它同層的前一個 sibling node 做合併。並會移除合併過後所產生的空節點。
參數:
editor: Editor
options: MergeNodesOptions
options :
interface MergeNodesOptions extends NodeOptions {
hanging?: boolean
}
hanging
與 insertNodes
的 hanging
option 一樣,決定是否將 at
為 Range 時的 value 修正成為 unhanging
type 。
moveNodes
用途:將單個/複數個節點從舊的 Location 搬遷到新的 Path
參數:
editor: Editor
options: MoveNodesOptions
options :
interface MoveNodesOptions extends NodeOptions {
to: Path
}
to
欲將 at
指向的節點搬遷到的新路徑( Path )
removeNodes
用途:將 at
Location 指向的單個/複數個節點從 Document 中移除
參數:
editor: Editor
options: RemoveNodesOptions
options :
interface RemoveNodesOptions extends NodeOptions {
hanging?: boolean
}
hanging
與 insertNodes
的 hanging
option 一樣,決定是否將 at
為 Range 時的 value 修正成為 unhanging
type 。
setNodes
用途:為 at
Location 指向的節點設置新屬性
參數:
editor: Editor
props: Partial<Node>
欲設置的新屬性
options: SetNodesOptions
options :
interface SetNodesOptions extends NodeOptions {
hanging?: boolean
split?: boolean
}
hanging
與 insertNodes
的 hanging
option 一樣,決定是否將 at
為 Range 時的 value 修正成為 unhanging
type 。
split
當 at
為 Range type 時,決定是否將節點拆分開來。
splitNodes
用途:拆分 Location 指向的節點。
參數:
editor: Editor
options: SplitNodesOptions
options :
interface SplitNodesOptions extends NodeOptions {
always?: boolean
height?: number
}
always
這個布林值會決定,如:作為基準的子節點的順位為整個子層節點的邊境(第一或最後順位)。這類實際上不需要拆分父層節點的狀況是否仍要強行拆分。
height
欲拆分的父層節點與 at
Location 指向的節點所相差的層級高度
unsetNodes
用途:取消 at
Location 指向的節點屬性設置
參數:
editor: Editor
props: Partial<Node>
欲取消設置的屬性
options: UnsetNodesOptions
options :
interface UnsetNodesOptions extends NodeOptions {
split?: boolean
}
split
這個 method 基本上就是多經過一層簡單的處理後,呼叫 setNodes
method ,因此這裡的 split
option 只是原封不動地作為傳給 setNodes
method 的參數而已。
unwrapNodes
用途:將 at
Location 指向的節點內容展開並提升至上一層的位置,如果傳入的 at
為 Range type 則會拆分父層節點,為了確保只有展開 Range 涵蓋的內容
參數:
editor: Editor
options: UnwrapNodesOptions
options :
interface UnwrapNodesOptions extends NodeOptions {
split?: boolean
}
split
當 at
為 Range type 時,決定是否將節點拆分開來。
這個 method 主要的工作內容是因應各種傳入的 at
與 match
參數來決定要丟入進 Transforms.liftNodes
method 的內容。
如果傳入的 at
為 Path ,要提升的內容則為 Path 指向的節點涵蓋到的所有文字作為 Range 傳入到 Transforms.liftNodes
。
如果傳入的 at
為 Range type 同時 split
參數設為 true
,才會去尋找 at
Range 與欲展開的節點之間的文字交集,並丟入到 Transforms.liftNodes
由它來展開 Range 內的文字內容並拆分父層節點,否則傳入的 Range 仍會以要提升的節點為單位去涵蓋節點內的所有文字。
wrapNodes
用途:將 element
節點裡的 at
Location 指向的內容包裝進一個新的 container 節點
參數:
editor: Editor
element: Element
涵蓋了 at
Location 的父層 container 節點,因應不同的 Block-type 會決定後續遍歷節點所傳入的 match
參數:
if (match == null) {
if (Path.isPath(at)) {
match = matchPath(editor, at)
} else if (editor.isInline(element)) {
match = n => Editor.isInline(editor, n) || Text.isText(n)
} else {
match = n => Editor.isBlock(editor, n)
}
}
// ...
const matches = Array.from(
Editor.nodes(editor, { at: a, match, mode, voids })
)
if (matches.length > 0) {
// wrapNodes implementation
}
options: WrapNodesOptions
options :
interface WrapNodesOptions extends NodeOptions {
split?: boolean
}
split
當 at
為 Range type 時,決定是否將節點拆分開來。
如果傳入的 at 為 Range type 同時 split 參數設為 true ,則會先將 Range 所涵蓋到的文字範圍與其之外的文字邊界先做節點拆分,確保只有 at 涵蓋到的文字集合被包裝進新的 container 節點:
if (split && Range.isRange(at)) {
const [start, end] = Range.edges(at)
const rangeRef = Editor.rangeRef(editor, at, {
affinity: 'inward',
})
Transforms.splitNodes(editor, { at: end, match, voids })
Transforms.splitNodes(editor, { at: start, match, voids })
at = rangeRef.unref()!
if (options.at == null) {
Transforms.select(editor, at)
}
}